/*
 * Copyright 2010 Srikanth Reddy Lingala
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.lingala.zip4j.core;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.io.SplitOutputStream;
import net.lingala.zip4j.model.AESExtraDataRecord;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.LocalFileHeader;
import net.lingala.zip4j.model.Zip64EndCentralDirLocator;
import net.lingala.zip4j.model.Zip64EndCentralDirRecord;
import net.lingala.zip4j.model.ZipModel;
import net.lingala.zip4j.util.InternalZipConstants;
import net.lingala.zip4j.util.Raw;
import net.lingala.zip4j.util.Zip4jUtil;

public class HeaderWriter {

    private final int ZIP64_EXTRA_BUF = 50;

    public int writeLocalFileHeader(ZipModel zipModel, LocalFileHeader localFileHeader,
                                    OutputStream outputStream) throws ZipException {
        if (localFileHeader == null) {
            throw new ZipException("input parameters are null, cannot write local file header");
        }

        try {
            ArrayList byteArrayList = new ArrayList();

            int headerLength = 0;

            byte[] shortByte = new byte[2];
            byte[] intByte = new byte[4];
            byte[] longByte = new byte[8];
            byte[] emptyLongByte = {0, 0, 0, 0, 0, 0, 0, 0};

            Raw.writeIntLittleEndian(intByte, 0, localFileHeader.getSignature());
            copyByteArrayToArrayList(intByte, byteArrayList);
            headerLength += 4;

            Raw.writeShortLittleEndian(shortByte, 0, (short) localFileHeader.getVersionNeededToExtract());
            copyByteArrayToArrayList(shortByte, byteArrayList);
            headerLength += 2;

            //General Purpose bit flags
            copyByteArrayToArrayList(localFileHeader.getGeneralPurposeFlag(), byteArrayList);
            headerLength += 2;

            //Compression Method
            Raw.writeShortLittleEndian(shortByte, 0, (short) localFileHeader.getCompressionMethod());
            copyByteArrayToArrayList(shortByte, byteArrayList);
            headerLength += 2;

            //File modified time
            int dateTime = localFileHeader.getLastModFileTime();
            Raw.writeIntLittleEndian(intByte, 0, (int) dateTime);
            copyByteArrayToArrayList(intByte, byteArrayList);
            headerLength += 4;

            //Skip crc for now - this field will be updated after data is compressed
            Raw.writeIntLittleEndian(intByte, 0, (int) localFileHeader.getCrc32());
            copyByteArrayToArrayList(intByte, byteArrayList);
            headerLength += 4;

            boolean writingZip64Rec = false;

            //compressed & uncompressed size
            long uncompressedSize = localFileHeader.getUncompressedSize();
            if (uncompressedSize + ZIP64_EXTRA_BUF >= InternalZipConstants.ZIP_64_LIMIT) {
                Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT);
                System.arraycopy(longByte, 0, intByte, 0, 4);

                //Set the uncompressed size to ZipConstants.ZIP_64_LIMIT as
                //these values will be stored in Zip64 extra record
                copyByteArrayToArrayList(intByte, byteArrayList);

                copyByteArrayToArrayList(intByte, byteArrayList);
                zipModel.setZip64Format(true);
                writingZip64Rec = true;
                localFileHeader.setWriteComprSizeInZip64ExtraRecord(true);
            } else {
                Raw.writeLongLittleEndian(longByte, 0, localFileHeader.getCompressedSize());
                System.arraycopy(longByte, 0, intByte, 0, 4);
                copyByteArrayToArrayList(intByte, byteArrayList);

                Raw.writeLongLittleEndian(longByte, 0, localFileHeader.getUncompressedSize());
                System.arraycopy(longByte, 0, intByte, 0, 4);
                //Raw.writeIntLittleEndian(intByte, 0, (int)localFileHeader.getUncompressedSize());
                copyByteArrayToArrayList(intByte, byteArrayList);

                localFileHeader.setWriteComprSizeInZip64ExtraRecord(false);
            }
            headerLength += 8;

            Raw.writeShortLittleEndian(shortByte, 0, (short) localFileHeader.getFileNameLength());
            copyByteArrayToArrayList(shortByte, byteArrayList);
            headerLength += 2;

            // extra field length
            int extraFieldLength = 0;
            if (writingZip64Rec) {
                extraFieldLength += 20;
            }
            if (localFileHeader.getAesExtraDataRecord() != null) {
                extraFieldLength += 11;
            }
            Raw.writeShortLittleEndian(shortByte, 0, (short) (extraFieldLength));
            copyByteArrayToArrayList(shortByte, byteArrayList);
            headerLength += 2;

            if (Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset())) {
                byte[] fileNameBytes = localFileHeader.getFileName().getBytes(zipModel.getFileNameCharset());
                copyByteArrayToArrayList(fileNameBytes, byteArrayList);
                headerLength += fileNameBytes.length;
            } else {
                copyByteArrayToArrayList(Zip4jUtil.convertCharset(localFileHeader.getFileName()), byteArrayList);
                headerLength += Zip4jUtil.getEncodedStringLength(localFileHeader.getFileName());
            }

            //Zip64 should be the first extra data record that should be written
            //This is NOT according to any specification but if this is changed
            //then take care of updateLocalFileHeader for compressed size
            if (writingZip64Rec) {


                //Zip64 header
                Raw.writeShortLittleEndian(shortByte, 0, (short) InternalZipConstants.EXTRAFIELDZIP64LENGTH);
                copyByteArrayToArrayList(shortByte, byteArrayList);
                headerLength += 2;

                //Zip64 extra data record size
                //hardcoded it to 16 for local file header as we will just write
                //compressed and uncompressed file sizes
                Raw.writeShortLittleEndian(shortByte, 0, (short) 16);
                copyByteArrayToArrayList(shortByte, byteArrayList);
                headerLength += 2;

                //uncompressed size
                Raw.writeLongLittleEndian(longByte, 0, localFileHeader.getUncompressedSize());
                copyByteArrayToArrayList(longByte, byteArrayList);
                headerLength += 8;

                //set compressed size to 0 for now
                copyByteArrayToArrayList(emptyLongByte, byteArrayList);
                headerLength += 8;
            }

            if (localFileHeader.getAesExtraDataRecord() != null) {
                AESExtraDataRecord aesExtraDataRecord = localFileHeader.getAesExtraDataRecord();

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getSignature());
                copyByteArrayToArrayList(shortByte, byteArrayList);

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getDataSize());
                copyByteArrayToArrayList(shortByte, byteArrayList);

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getVersionNumber());
                copyByteArrayToArrayList(shortByte, byteArrayList);

                copyByteArrayToArrayList(aesExtraDataRecord.getVendorID().getBytes(), byteArrayList);

                byte[] aesStrengthBytes = new byte[1];
                aesStrengthBytes[0] = (byte) aesExtraDataRecord.getAesStrength();
                copyByteArrayToArrayList(aesStrengthBytes, byteArrayList);

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getCompressionMethod());
                copyByteArrayToArrayList(shortByte, byteArrayList);
            }
            byte[] lhBytes = byteArrayListToByteArray(byteArrayList);
            outputStream.write(lhBytes);
            return lhBytes.length;
        } catch (ZipException e) {
            throw e;
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    public int writeExtendedLocalHeader(LocalFileHeader localFileHeader,
                                        OutputStream outputStream) throws ZipException, IOException {
        if (localFileHeader == null || outputStream == null) {
            throw new ZipException("input parameters is null, cannot write extended local header");
        }

        ArrayList byteArrayList = new ArrayList();
        byte[] intByte = new byte[4];

        //Extended local file header signature
        Raw.writeIntLittleEndian(intByte, 0, (int) InternalZipConstants.EXTSIG);
        copyByteArrayToArrayList(intByte, byteArrayList);

        //CRC
        Raw.writeIntLittleEndian(intByte, 0, (int) localFileHeader.getCrc32());
        copyByteArrayToArrayList(intByte, byteArrayList);

        //compressed size
        long compressedSize = localFileHeader.getCompressedSize();
        if (compressedSize >= Integer.MAX_VALUE) {
            compressedSize = Integer.MAX_VALUE;
        }
        Raw.writeIntLittleEndian(intByte, 0, (int) compressedSize);
        copyByteArrayToArrayList(intByte, byteArrayList);

        //uncompressed size
        long uncompressedSize = localFileHeader.getUncompressedSize();
        if (uncompressedSize >= Integer.MAX_VALUE) {
            uncompressedSize = Integer.MAX_VALUE;
        }
        Raw.writeIntLittleEndian(intByte, 0, (int) uncompressedSize);
        copyByteArrayToArrayList(intByte, byteArrayList);

        byte[] extLocHdrBytes = byteArrayListToByteArray(byteArrayList);
        outputStream.write(extLocHdrBytes);
        return extLocHdrBytes.length;
    }

    /**
     * Processes zip header data and writes this data to the zip file
     *
     * @param zipModel
     * @param outputStream
     * @throws ZipException
     */
    public void finalizeZipFile(ZipModel zipModel,
                                OutputStream outputStream) throws ZipException {
        if (zipModel == null || outputStream == null) {
            throw new ZipException("input parameters is null, cannot finalize zip file");
        }

        try {
            processHeaderData(zipModel, outputStream);

            long offsetCentralDir = zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir();

            List headerBytesList = new ArrayList();

            int sizeOfCentralDir = writeCentralDirectory(zipModel, outputStream, headerBytesList);

            if (zipModel.isZip64Format()) {
                if (zipModel.getZip64EndCentralDirRecord() == null) {
                    zipModel.setZip64EndCentralDirRecord(new Zip64EndCentralDirRecord());
                }
                if (zipModel.getZip64EndCentralDirLocator() == null) {
                    zipModel.setZip64EndCentralDirLocator(new Zip64EndCentralDirLocator());
                }

                zipModel.getZip64EndCentralDirLocator().setOffsetZip64EndOfCentralDirRec(offsetCentralDir + sizeOfCentralDir);
                if (outputStream instanceof SplitOutputStream) {
                    zipModel.getZip64EndCentralDirLocator().setNoOfDiskStartOfZip64EndOfCentralDirRec(((SplitOutputStream) outputStream).getCurrSplitFileCounter());
                    zipModel.getZip64EndCentralDirLocator().setTotNumberOfDiscs(((SplitOutputStream) outputStream).getCurrSplitFileCounter() + 1);
                } else {
                    zipModel.getZip64EndCentralDirLocator().setNoOfDiskStartOfZip64EndOfCentralDirRec(0);
                    zipModel.getZip64EndCentralDirLocator().setTotNumberOfDiscs(1);
                }

                writeZip64EndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList);

                writeZip64EndOfCentralDirectoryLocator(zipModel, outputStream, headerBytesList);
            }

            writeEndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList);

            writeZipHeaderBytes(zipModel, outputStream, byteArrayListToByteArray(headerBytesList));
        } catch (ZipException e) {
            throw e;
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    /**
     * Processes zip header data and writes this data to the zip file without any validations.
     * This process is not intended to use for normal operations (adding, deleting, etc) of a zip file.
     * This method is used when certain validations need to be skipped (ex: Merging split zip files,
     * adding comment to a zip file, etc)
     *
     * @param zipModel
     * @param outputStream
     * @throws ZipException
     */
    public void finalizeZipFileWithoutValidations(ZipModel zipModel, OutputStream outputStream) throws ZipException {
        if (zipModel == null || outputStream == null) {
            throw new ZipException("input parameters is null, cannot finalize zip file without validations");
        }

        try {

            List headerBytesList = new ArrayList();

            long offsetCentralDir = zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir();

            int sizeOfCentralDir = writeCentralDirectory(zipModel, outputStream, headerBytesList);

            if (zipModel.isZip64Format()) {
                if (zipModel.getZip64EndCentralDirRecord() == null) {
                    zipModel.setZip64EndCentralDirRecord(new Zip64EndCentralDirRecord());
                }
                if (zipModel.getZip64EndCentralDirLocator() == null) {
                    zipModel.setZip64EndCentralDirLocator(new Zip64EndCentralDirLocator());
                }

                zipModel.getZip64EndCentralDirLocator().setOffsetZip64EndOfCentralDirRec(offsetCentralDir + sizeOfCentralDir);

                writeZip64EndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList);
                writeZip64EndOfCentralDirectoryLocator(zipModel, outputStream, headerBytesList);
            }

            writeEndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList);

            writeZipHeaderBytes(zipModel, outputStream, byteArrayListToByteArray(headerBytesList));
        } catch (ZipException e) {
            throw e;
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    /**
     * Writes the zip header data to the zip file
     *
     * @param outputStream
     * @param buff
     * @throws ZipException
     */
    private void writeZipHeaderBytes(ZipModel zipModel, OutputStream outputStream, byte[] buff) throws ZipException {
        if (buff == null) {
            throw new ZipException("invalid buff to write as zip headers");
        }

        try {
            if (outputStream instanceof SplitOutputStream) {
                if (((SplitOutputStream) outputStream).checkBuffSizeAndStartNextSplitFile(buff.length)) {
                    finalizeZipFile(zipModel, outputStream);
                    return;
                }
            }

            outputStream.write(buff);
        } catch (IOException e) {
            throw new ZipException(e);
        }
    }

    /**
     * Fills the header data in the zip model
     *
     * @param zipModel
     * @param outputStream
     * @throws ZipException
     */
    private void processHeaderData(ZipModel zipModel, OutputStream outputStream) throws ZipException {
        try {
            int currSplitFileCounter = 0;
            if (outputStream instanceof SplitOutputStream) {
                zipModel.getEndCentralDirRecord().setOffsetOfStartOfCentralDir(
                        ((SplitOutputStream) outputStream).getFilePointer());
                currSplitFileCounter = ((SplitOutputStream) outputStream).getCurrSplitFileCounter();

            }

            if (zipModel.isZip64Format()) {
                if (zipModel.getZip64EndCentralDirRecord() == null) {
                    zipModel.setZip64EndCentralDirRecord(new Zip64EndCentralDirRecord());
                }
                if (zipModel.getZip64EndCentralDirLocator() == null) {
                    zipModel.setZip64EndCentralDirLocator(new Zip64EndCentralDirLocator());
                }

                zipModel.getZip64EndCentralDirLocator().setNoOfDiskStartOfZip64EndOfCentralDirRec(currSplitFileCounter);
                zipModel.getZip64EndCentralDirLocator().setTotNumberOfDiscs(currSplitFileCounter + 1);
            }
            zipModel.getEndCentralDirRecord().setNoOfThisDisk(currSplitFileCounter);
            zipModel.getEndCentralDirRecord().setNoOfThisDiskStartOfCentralDir(currSplitFileCounter);
        } catch (IOException e) {
            throw new ZipException(e);
        }
    }

    /**
     * Writes central directory header data to an array list
     *
     * @param zipModel
     * @param outputStream
     * @param headerBytesList
     * @return size of central directory
     * @throws ZipException
     */
    private int writeCentralDirectory(ZipModel zipModel,
                                      OutputStream outputStream, List headerBytesList) throws ZipException {
        if (zipModel == null || outputStream == null) {
            throw new ZipException("input parameters is null, cannot write central directory");
        }

        if (zipModel.getCentralDirectory() == null ||
                zipModel.getCentralDirectory().getFileHeaders() == null ||
                zipModel.getCentralDirectory().getFileHeaders().size() <= 0) {
            return 0;
        }

        int sizeOfCentralDir = 0;
        for (int i = 0; i < zipModel.getCentralDirectory().getFileHeaders().size(); i++) {
            FileHeader fileHeader = (FileHeader) zipModel.getCentralDirectory().getFileHeaders().get(i);
            int sizeOfFileHeader = writeFileHeader(zipModel, fileHeader, outputStream, headerBytesList);
            sizeOfCentralDir += sizeOfFileHeader;
        }
        return sizeOfCentralDir;
    }

    private int writeFileHeader(ZipModel zipModel, FileHeader fileHeader,
                                OutputStream outputStream, List headerBytesList) throws ZipException {

        if (fileHeader == null || outputStream == null) {
            throw new ZipException("input parameters is null, cannot write local file header");
        }

        try {
            int sizeOfFileHeader = 0;

            byte[] shortByte = new byte[2];
            byte[] intByte = new byte[4];
            byte[] longByte = new byte[8];
            final byte[] emptyShortByte = {0, 0};
            final byte[] emptyIntByte = {0, 0, 0, 0};

            boolean writeZip64FileSize = false;
            boolean writeZip64OffsetLocalHeader = false;

            Raw.writeIntLittleEndian(intByte, 0, fileHeader.getSignature());
            copyByteArrayToArrayList(intByte, headerBytesList);
            sizeOfFileHeader += 4;

            Raw.writeShortLittleEndian(shortByte, 0, (short) fileHeader.getVersionMadeBy());
            copyByteArrayToArrayList(shortByte, headerBytesList);
            sizeOfFileHeader += 2;

            Raw.writeShortLittleEndian(shortByte, 0, (short) fileHeader.getVersionNeededToExtract());
            copyByteArrayToArrayList(shortByte, headerBytesList);
            sizeOfFileHeader += 2;

            copyByteArrayToArrayList(fileHeader.getGeneralPurposeFlag(), headerBytesList);
            sizeOfFileHeader += 2;

            Raw.writeShortLittleEndian(shortByte, 0, (short) fileHeader.getCompressionMethod());
            copyByteArrayToArrayList(shortByte, headerBytesList);
            sizeOfFileHeader += 2;

            int dateTime = fileHeader.getLastModFileTime();
            Raw.writeIntLittleEndian(intByte, 0, dateTime);
            copyByteArrayToArrayList(intByte, headerBytesList);
            sizeOfFileHeader += 4;

            Raw.writeIntLittleEndian(intByte, 0, (int) (fileHeader.getCrc32()));
            copyByteArrayToArrayList(intByte, headerBytesList);
            sizeOfFileHeader += 4;

            if (fileHeader.getCompressedSize() >= InternalZipConstants.ZIP_64_LIMIT ||
                    fileHeader.getUncompressedSize() + ZIP64_EXTRA_BUF >= InternalZipConstants.ZIP_64_LIMIT) {
                Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT);
                System.arraycopy(longByte, 0, intByte, 0, 4);

                copyByteArrayToArrayList(intByte, headerBytesList);
                sizeOfFileHeader += 4;

                copyByteArrayToArrayList(intByte, headerBytesList);
                sizeOfFileHeader += 4;

                writeZip64FileSize = true;
            } else {
                Raw.writeLongLittleEndian(longByte, 0, fileHeader.getCompressedSize());
                System.arraycopy(longByte, 0, intByte, 0, 4);
//				Raw.writeIntLittleEndian(intByte, 0, (int)fileHeader.getCompressedSize());
                copyByteArrayToArrayList(intByte, headerBytesList);
                sizeOfFileHeader += 4;

                Raw.writeLongLittleEndian(longByte, 0, fileHeader.getUncompressedSize());
                System.arraycopy(longByte, 0, intByte, 0, 4);
//				Raw.writeIntLittleEndian(intByte, 0, (int)fileHeader.getUncompressedSize());
                copyByteArrayToArrayList(intByte, headerBytesList);
                sizeOfFileHeader += 4;
            }

            Raw.writeShortLittleEndian(shortByte, 0, (short) fileHeader.getFileNameLength());
            copyByteArrayToArrayList(shortByte, headerBytesList);
            sizeOfFileHeader += 2;

            //Compute offset bytes before extra field is written for Zip64 compatibility
            //NOTE: this data is not written now, but written at a later point
            byte[] offsetLocalHeaderBytes = new byte[4];
            if (fileHeader.getOffsetLocalHeader() > InternalZipConstants.ZIP_64_LIMIT) {
                Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT);
                System.arraycopy(longByte, 0, offsetLocalHeaderBytes, 0, 4);
                writeZip64OffsetLocalHeader = true;
            } else {
                Raw.writeLongLittleEndian(longByte, 0, fileHeader.getOffsetLocalHeader());
                System.arraycopy(longByte, 0, offsetLocalHeaderBytes, 0, 4);
            }

            // extra field length
            int extraFieldLength = 0;
            if (writeZip64FileSize || writeZip64OffsetLocalHeader) {
                extraFieldLength += 4;
                if (writeZip64FileSize)
                    extraFieldLength += 16;
                if (writeZip64OffsetLocalHeader)
                    extraFieldLength += 8;
            }
            if (fileHeader.getAesExtraDataRecord() != null) {
                extraFieldLength += 11;
            }
            Raw.writeShortLittleEndian(shortByte, 0, (short) (extraFieldLength));
            copyByteArrayToArrayList(shortByte, headerBytesList);
            sizeOfFileHeader += 2;

            //Skip file comment length for now
            copyByteArrayToArrayList(emptyShortByte, headerBytesList);
            sizeOfFileHeader += 2;

            //Skip disk number start for now
            Raw.writeShortLittleEndian(shortByte, 0, (short) (fileHeader.getDiskNumberStart()));
            copyByteArrayToArrayList(shortByte, headerBytesList);
            sizeOfFileHeader += 2;

            //Skip internal file attributes for now
            copyByteArrayToArrayList(emptyShortByte, headerBytesList);
            sizeOfFileHeader += 2;

            //External file attributes
            if (fileHeader.getExternalFileAttr() != null) {
                copyByteArrayToArrayList(fileHeader.getExternalFileAttr(), headerBytesList);
            } else {
                copyByteArrayToArrayList(emptyIntByte, headerBytesList);
            }
            sizeOfFileHeader += 4;

            //offset local header
            //this data is computed above
            copyByteArrayToArrayList(offsetLocalHeaderBytes, headerBytesList);
            sizeOfFileHeader += 4;

            if (Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset())) {
                byte[] fileNameBytes = fileHeader.getFileName().getBytes(zipModel.getFileNameCharset());
                copyByteArrayToArrayList(fileNameBytes, headerBytesList);
                sizeOfFileHeader += fileNameBytes.length;
            } else {
                copyByteArrayToArrayList(Zip4jUtil.convertCharset(fileHeader.getFileName()), headerBytesList);
                sizeOfFileHeader += Zip4jUtil.getEncodedStringLength(fileHeader.getFileName());
            }

            if (writeZip64FileSize || writeZip64OffsetLocalHeader) {
                zipModel.setZip64Format(true);

                //Zip64 header
                Raw.writeShortLittleEndian(shortByte, 0, (short) InternalZipConstants.EXTRAFIELDZIP64LENGTH);
                copyByteArrayToArrayList(shortByte, headerBytesList);
                sizeOfFileHeader += 2;

                //Zip64 extra data record size
                int dataSize = 0;

                if (writeZip64FileSize) {
                    dataSize += 16;
                }
                if (writeZip64OffsetLocalHeader) {
                    dataSize += 8;
                }

                Raw.writeShortLittleEndian(shortByte, 0, (short) dataSize);
                copyByteArrayToArrayList(shortByte, headerBytesList);
                sizeOfFileHeader += 2;

                if (writeZip64FileSize) {
                    Raw.writeLongLittleEndian(longByte, 0, fileHeader.getUncompressedSize());
                    copyByteArrayToArrayList(longByte, headerBytesList);
                    sizeOfFileHeader += 8;

                    Raw.writeLongLittleEndian(longByte, 0, fileHeader.getCompressedSize());
                    copyByteArrayToArrayList(longByte, headerBytesList);
                    sizeOfFileHeader += 8;
                }

                if (writeZip64OffsetLocalHeader) {
                    Raw.writeLongLittleEndian(longByte, 0, fileHeader.getOffsetLocalHeader());
                    copyByteArrayToArrayList(longByte, headerBytesList);
                    sizeOfFileHeader += 8;
                }
            }

            if (fileHeader.getAesExtraDataRecord() != null) {
                AESExtraDataRecord aesExtraDataRecord = fileHeader.getAesExtraDataRecord();

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getSignature());
                copyByteArrayToArrayList(shortByte, headerBytesList);

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getDataSize());
                copyByteArrayToArrayList(shortByte, headerBytesList);

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getVersionNumber());
                copyByteArrayToArrayList(shortByte, headerBytesList);

                copyByteArrayToArrayList(aesExtraDataRecord.getVendorID().getBytes(), headerBytesList);

                byte[] aesStrengthBytes = new byte[1];
                aesStrengthBytes[0] = (byte) aesExtraDataRecord.getAesStrength();
                copyByteArrayToArrayList(aesStrengthBytes, headerBytesList);

                Raw.writeShortLittleEndian(shortByte, 0, (short) aesExtraDataRecord.getCompressionMethod());
                copyByteArrayToArrayList(shortByte, headerBytesList);

                sizeOfFileHeader += 11;
            }

//			outputStream.write(byteArrayListToByteArray(headerBytesList));

            return sizeOfFileHeader;
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    private void writeZip64EndOfCentralDirectoryRecord(ZipModel zipModel,
                                                       OutputStream outputStream, int sizeOfCentralDir,
                                                       long offsetCentralDir, List headerBytesList) throws ZipException {
        if (zipModel == null || outputStream == null) {
            throw new ZipException("zip model or output stream is null, cannot write zip64 end of central directory record");
        }

        try {

            byte[] shortByte = new byte[2];
            byte[] emptyShortByte = {0, 0};
            byte[] intByte = new byte[4];
            byte[] longByte = new byte[8];

            //zip64 end of central dir signature
            Raw.writeIntLittleEndian(intByte, 0, (int) InternalZipConstants.ZIP64ENDCENDIRREC);
            copyByteArrayToArrayList(intByte, headerBytesList);

            //size of zip64 end of central directory record
            Raw.writeLongLittleEndian(longByte, 0, (long) 44);
            copyByteArrayToArrayList(longByte, headerBytesList);

            //version made by
            //version needed to extract
            if (zipModel.getCentralDirectory() != null &&
                    zipModel.getCentralDirectory().getFileHeaders() != null &&
                    zipModel.getCentralDirectory().getFileHeaders().size() > 0) {
                Raw.writeShortLittleEndian(shortByte, 0,
                        (short) ((FileHeader) zipModel.getCentralDirectory().getFileHeaders().get(0)).getVersionMadeBy());
                copyByteArrayToArrayList(shortByte, headerBytesList);

                Raw.writeShortLittleEndian(shortByte, 0,
                        (short) ((FileHeader) zipModel.getCentralDirectory().getFileHeaders().get(0)).getVersionNeededToExtract());
                copyByteArrayToArrayList(shortByte, headerBytesList);
            } else {
                copyByteArrayToArrayList(emptyShortByte, headerBytesList);
                copyByteArrayToArrayList(emptyShortByte, headerBytesList);
            }

            //number of this disk
            Raw.writeIntLittleEndian(intByte, 0, zipModel.getEndCentralDirRecord().getNoOfThisDisk());
            copyByteArrayToArrayList(intByte, headerBytesList);

            //number of the disk with start of central directory
            Raw.writeIntLittleEndian(intByte, 0, zipModel.getEndCentralDirRecord().getNoOfThisDiskStartOfCentralDir());
            copyByteArrayToArrayList(intByte, headerBytesList);

            //total number of entries in the central directory on this disk
            int numEntries = 0;
            int numEntriesOnThisDisk = 0;
            if (zipModel.getCentralDirectory() == null ||
                    zipModel.getCentralDirectory().getFileHeaders() == null) {
                throw new ZipException("invalid central directory/file headers, " +
                        "cannot write end of central directory record");
            } else {
                numEntries = zipModel.getCentralDirectory().getFileHeaders().size();
                if (zipModel.isSplitArchive()) {
                    countNumberOfFileHeaderEntriesOnDisk(zipModel.getCentralDirectory().getFileHeaders(),
                            zipModel.getEndCentralDirRecord().getNoOfThisDisk());
                } else {
                    numEntriesOnThisDisk = numEntries;
                }
            }
            Raw.writeLongLittleEndian(longByte, 0, numEntriesOnThisDisk);
            copyByteArrayToArrayList(longByte, headerBytesList);

            //Total number of entries in central directory
            Raw.writeLongLittleEndian(longByte, 0, numEntries);
            copyByteArrayToArrayList(longByte, headerBytesList);

            //Size of central directory
            Raw.writeLongLittleEndian(longByte, 0, sizeOfCentralDir);
            copyByteArrayToArrayList(longByte, headerBytesList);

            //offset of start of central directory with respect to the starting disk number
            Raw.writeLongLittleEndian(longByte, 0, offsetCentralDir);
            copyByteArrayToArrayList(longByte, headerBytesList);

        } catch (ZipException zipException) {
            throw zipException;
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    private void writeZip64EndOfCentralDirectoryLocator(ZipModel zipModel,
                                                        OutputStream outputStream, List headerBytesList) throws ZipException {
        if (zipModel == null || outputStream == null) {
            throw new ZipException("zip model or output stream is null, cannot write zip64 end of central directory locator");
        }

        try {

            byte[] intByte = new byte[4];
            byte[] longByte = new byte[8];

            //zip64 end of central dir locator  signature
            Raw.writeIntLittleEndian(intByte, 0, (int) InternalZipConstants.ZIP64ENDCENDIRLOC);
            copyByteArrayToArrayList(intByte, headerBytesList);

            //number of the disk with the start of the zip64 end of central directory
            Raw.writeIntLittleEndian(intByte, 0, zipModel.getZip64EndCentralDirLocator().getNoOfDiskStartOfZip64EndOfCentralDirRec());
            copyByteArrayToArrayList(intByte, headerBytesList);

            //relative offset of the zip64 end of central directory record
            Raw.writeLongLittleEndian(longByte, 0, zipModel.getZip64EndCentralDirLocator().getOffsetZip64EndOfCentralDirRec());
            copyByteArrayToArrayList(longByte, headerBytesList);

            //total number of disks
            Raw.writeIntLittleEndian(intByte, 0, zipModel.getZip64EndCentralDirLocator().getTotNumberOfDiscs());
            copyByteArrayToArrayList(intByte, headerBytesList);
        } catch (ZipException zipException) {
            throw zipException;
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    private void writeEndOfCentralDirectoryRecord(ZipModel zipModel,
                                                  OutputStream outputStream,
                                                  int sizeOfCentralDir,
                                                  long offsetCentralDir,
                                                  List headrBytesList) throws ZipException {
        if (zipModel == null || outputStream == null) {
            throw new ZipException("zip model or output stream is null, cannot write end of central directory record");
        }

        try {

            byte[] shortByte = new byte[2];
            byte[] intByte = new byte[4];
            byte[] longByte = new byte[8];

            //End of central directory signature
            Raw.writeIntLittleEndian(intByte, 0, (int) zipModel.getEndCentralDirRecord().getSignature());
            copyByteArrayToArrayList(intByte, headrBytesList);

            //number of this disk
            Raw.writeShortLittleEndian(shortByte, 0, (short) (zipModel.getEndCentralDirRecord().getNoOfThisDisk()));
            copyByteArrayToArrayList(shortByte, headrBytesList);

            //number of the disk with start of central directory
            Raw.writeShortLittleEndian(shortByte, 0, (short) (zipModel.getEndCentralDirRecord().getNoOfThisDiskStartOfCentralDir()));
            copyByteArrayToArrayList(shortByte, headrBytesList);

            //Total number of entries in central directory on this disk
            int numEntries = 0;
            int numEntriesOnThisDisk = 0;
            if (zipModel.getCentralDirectory() == null ||
                    zipModel.getCentralDirectory().getFileHeaders() == null) {
                throw new ZipException("invalid central directory/file headers, " +
                        "cannot write end of central directory record");
            } else {
                numEntries = zipModel.getCentralDirectory().getFileHeaders().size();
                if (zipModel.isSplitArchive()) {
                    numEntriesOnThisDisk = countNumberOfFileHeaderEntriesOnDisk(zipModel.getCentralDirectory().getFileHeaders(),
                            zipModel.getEndCentralDirRecord().getNoOfThisDisk());
                } else {
                    numEntriesOnThisDisk = numEntries;
                }
            }
            Raw.writeShortLittleEndian(shortByte, 0, (short) numEntriesOnThisDisk);
            copyByteArrayToArrayList(shortByte, headrBytesList);

            //Total number of entries in central directory
            Raw.writeShortLittleEndian(shortByte, 0, (short) numEntries);
            copyByteArrayToArrayList(shortByte, headrBytesList);

            //Size of central directory
            Raw.writeIntLittleEndian(intByte, 0, sizeOfCentralDir);
            copyByteArrayToArrayList(intByte, headrBytesList);

            //Offset central directory
            if (offsetCentralDir > InternalZipConstants.ZIP_64_LIMIT) {
                Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT);
                System.arraycopy(longByte, 0, intByte, 0, 4);
                copyByteArrayToArrayList(intByte, headrBytesList);
            } else {
                Raw.writeLongLittleEndian(longByte, 0, offsetCentralDir);
                System.arraycopy(longByte, 0, intByte, 0, 4);
//				Raw.writeIntLittleEndian(intByte, 0, (int)offsetCentralDir);
                copyByteArrayToArrayList(intByte, headrBytesList);
            }

            //Zip File comment length
            int commentLength = 0;
            if (zipModel.getEndCentralDirRecord().getComment() != null) {
                commentLength = zipModel.getEndCentralDirRecord().getCommentLength();
            }
            Raw.writeShortLittleEndian(shortByte, 0, (short) commentLength);
            copyByteArrayToArrayList(shortByte, headrBytesList);

            //Comment
            if (commentLength > 0) {
                copyByteArrayToArrayList(zipModel.getEndCentralDirRecord().getCommentBytes(), headrBytesList);
            }

        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    public void updateLocalFileHeader(LocalFileHeader localFileHeader, long offset,
                                      int toUpdate, ZipModel zipModel, byte[] bytesToWrite, int noOfDisk, SplitOutputStream outputStream) throws ZipException {
        if (localFileHeader == null || offset < 0 || zipModel == null) {
            throw new ZipException("invalid input parameters, cannot update local file header");
        }

        try {
            boolean closeFlag = false;
            SplitOutputStream currOutputStream = null;

            if (noOfDisk != outputStream.getCurrSplitFileCounter()) {
                File zipFile = new File(zipModel.getZipFile());
                String parentFile = zipFile.getParent();
                String fileNameWithoutExt = Zip4jUtil.getZipFileNameWithoutExt(zipFile.getName());
                String fileName = parentFile + System.getProperty("file.separator");
                if (noOfDisk < 9) {
                    fileName += fileNameWithoutExt + ".z0" + (noOfDisk + 1);
                } else {
                    fileName += fileNameWithoutExt + ".z" + (noOfDisk + 1);
                }
                currOutputStream = new SplitOutputStream(new File(fileName));
                closeFlag = true;
            } else {
                currOutputStream = outputStream;
            }

            long currOffset = currOutputStream.getFilePointer();

            if (currOutputStream == null) {
                throw new ZipException("invalid output stream handler, cannot update local file header");
            }

            switch (toUpdate) {
                case InternalZipConstants.UPDATE_LFH_CRC:
                    currOutputStream.seek(offset + toUpdate);
                    currOutputStream.write(bytesToWrite);
                    break;
                case InternalZipConstants.UPDATE_LFH_COMP_SIZE:
                case InternalZipConstants.UPDATE_LFH_UNCOMP_SIZE:
                    updateCompressedSizeInLocalFileHeader(currOutputStream, localFileHeader,
                            offset, toUpdate, bytesToWrite, zipModel.isZip64Format());
                    break;
                default:
                    break;
            }
            if (closeFlag) {
                currOutputStream.close();
            } else {
                outputStream.seek(currOffset);
            }

        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    private void updateCompressedSizeInLocalFileHeader(SplitOutputStream outputStream, LocalFileHeader localFileHeader,
                                                       long offset, long toUpdate, byte[] bytesToWrite, boolean isZip64Format) throws ZipException {

        if (outputStream == null) {
            throw new ZipException("invalid output stream, cannot update compressed size for local file header");
        }

        try {
            if (localFileHeader.isWriteComprSizeInZip64ExtraRecord()) {
                if (bytesToWrite.length != 8) {
                    throw new ZipException("attempting to write a non 8-byte compressed size block for a zip64 file");
                }

                //4 - compressed size
                //4 - uncomprssed size
                //2 - file name length
                //2 - extra field length
                //file name length
                //2 - Zip64 signature
                //2 - size of zip64 data
                //8 - crc
                //8 - compressed size
                //8 - uncompressed size
                long zip64CompressedSizeOffset = offset + toUpdate + 4 + 4 + 2 + 2 + localFileHeader.getFileNameLength() + 2 + 2 + 8;
                if (toUpdate == InternalZipConstants.UPDATE_LFH_UNCOMP_SIZE) {
                    zip64CompressedSizeOffset += 8;
                }
                outputStream.seek(zip64CompressedSizeOffset);
                outputStream.write(bytesToWrite);
            } else {
                outputStream.seek(offset + toUpdate);
                outputStream.write(bytesToWrite);
            }
        } catch (IOException e) {
            throw new ZipException(e);
        }

    }

    private void copyByteArrayToArrayList(byte[] byteArray, List arrayList) throws ZipException {
        if (arrayList == null || byteArray == null) {
            throw new ZipException("one of the input parameters is null, cannot copy byte array to array list");
        }

        for (int i = 0; i < byteArray.length; i++) {
            arrayList.add(Byte.toString(byteArray[i]));
        }
    }

    private byte[] byteArrayListToByteArray(List arrayList) throws ZipException {
        if (arrayList == null) {
            throw new ZipException("input byte array list is null, cannot conver to byte array");
        }

        if (arrayList.size() <= 0) {
            return null;
        }

        byte[] retBytes = new byte[arrayList.size()];

        for (int i = 0; i < arrayList.size(); i++) {
            retBytes[i] = Byte.parseByte((String) arrayList.get(i));
        }

        return retBytes;
    }

    private int countNumberOfFileHeaderEntriesOnDisk(ArrayList fileHeaders,
                                                     int numOfDisk) throws ZipException {
        if (fileHeaders == null) {
            throw new ZipException("file headers are null, cannot calculate number of entries on this disk");
        }

        int noEntries = 0;
        for (int i = 0; i < fileHeaders.size(); i++) {
            FileHeader fileHeader = (FileHeader) fileHeaders.get(i);
            if (fileHeader.getDiskNumberStart() == numOfDisk) {
                noEntries++;
            }
        }
        return noEntries;
    }

}